//
//  LcPersistenceCenter.m
//  18Birdies
//
//  Created by Lc on 15/3/23.
//  Copyright (c) 2015年 Lc. All rights reserved.
//

#include <sys/mount.h>
#import <zlib.h>

#import "LcJson.h"
#import "LcLog.h"
#import "LcPersistenceCenter.h"
#import "LcPersistenceException.h"
#import "LcPersistenceMeta.h"
#import "LcPreconditions.h"
#import "LcReflection.h"
#import "LcSecurityCenter.h"

@interface LcPersistenceCenter ()
@property(nonatomic, strong) NSFileManager* fileManager;
@property(nonatomic, strong) LcJson* lcJson;
@property(nonatomic, strong) LcSecurityCenter* securityCenter;
@property(nonatomic, strong) NSData* aesKey;
@property(nonatomic, strong) NSString* aesIv;
@end

@implementation LcPersistenceCenter

#pragma mark - Single Instance

- (void)internalInit {
  self.fileManager = [NSFileManager defaultManager];
  NSString* documentDirectory = NSSearchPathForDirectoriesInDomains(
      NSDocumentDirectory, NSUserDomainMask, YES)[0];
  _rootDirectory =
      [documentDirectory stringByAppendingPathComponent:@"LcFoundation"].copy;
  LcLog(@"rootDirectory=%@", self.rootDirectory);
  if (![self.fileManager fileExistsAtPath:self.rootDirectory]) {
    LC_CHECK_TRUE([self.fileManager createDirectoryAtPath:self.rootDirectory
                              withIntermediateDirectories:NO
                                               attributes:nil
                                                    error:nil]);
  }
  self.lcJson = [LcJson sharedInstance];
  self.securityCenter = [LcSecurityCenter sharedInstance];
  // NEVER change this
  char aesKeyBytes[] = {
      80, 58, -62, 43, 106, 2, -108, -61, 95, 113, -32, -104, -35, -9, 57, -94,
  };
  self.aesKey = [NSData dataWithBytes:aesKeyBytes length:16];
  // NEVER change this
  self.aesIv = @"*LcFoundationIV*";
}

#pragma mark -

- (id)objectForPersistenceMeta:(LcPersistenceMeta*)persistenceMeta {
  LC_CHECK_NOT_NULL(persistenceMeta);
  NSString* filePath = [self.rootDirectory
      stringByAppendingPathComponent:persistenceMeta.fileName];
  if (![self.fileManager fileExistsAtPath:filePath]) {
    return nil;
  }
  NSData* data = [NSData dataWithContentsOfFile:filePath];
  if (persistenceMeta.encrypting) {
    data = [self decrypt:data];
  }
  if (persistenceMeta.compressing) {
    data = [self uncompress:data];
  }
  return [self.lcJson fromJson:data
                       ofClass:NSClassFromString(persistenceMeta.type)
              withGenericTypes:persistenceMeta.genericTypes];
}

- (void)saveObject:(id)value
forPersistenceMeta:(LcPersistenceMeta*)persistenceMeta {
  LC_CHECK_NOT_NULL(persistenceMeta);
  if (value == nil) {
    [self remove:persistenceMeta];
    return;
  }

  [self assertValue:value shouldBeType:persistenceMeta.type];

  NSData* data = [self.lcJson toJson:value
                             ofClass:[value class]
                    withGenericTypes:persistenceMeta.genericTypes];
  if (persistenceMeta.compressing) {
    data = [self compress:data];
  }
  if (persistenceMeta.encrypting) {
    data = [self encrypt:data];
  }
  NSString* filePath = [self.rootDirectory
      stringByAppendingPathComponent:persistenceMeta.fileName];
  // LcLog(@"filePath=%@", filePath);
  NSString* directory = [filePath stringByDeletingLastPathComponent];
  @synchronized(self) {
    if (![self.fileManager fileExistsAtPath:directory]) {
      LC_CHECK_TRUE([self.fileManager createDirectoryAtPath:directory
                                withIntermediateDirectories:YES
                                                 attributes:nil
                                                      error:nil]);
    }
    if (![data writeToFile:filePath atomically:YES]) {
      @throw [LcPersistenceException
              exceptionWithReason:
              [NSString stringWithFormat:@"persistence write failed, fileName=%@",
               persistenceMeta.fileName]];
    }
  }
}

- (void)assertValue:(id)value shouldBeType:(NSString*)type {
  Class clazz = NSClassFromString(type);
  if (clazz == NULL) {
    @throw [LcPersistenceException
        exceptionWithReason:
            [NSString
                stringWithFormat:@"fatal error, type %@ is not class", type]];
  }
  if (![LcReflection isType:NSStringFromClass([value class])
                KindOfClass:clazz]) {
    @throw [LcPersistenceException
        exceptionWithReason:[NSString
                                stringWithFormat:@"value is not the type of %@",
                                                 type]];
  }
}

- (void)remove:(LcPersistenceMeta*)persistenceMeta {
  LC_CHECK_NOT_NULL(persistenceMeta);
  [self removeFile:persistenceMeta.fileName];
}

- (void)saveData:(NSData*)data toFile:(NSString*)fileName {
  LC_CHECK_NOT_EMPTY(data);
  LC_CHECK_NOT_EMPTY(fileName);
  [self saveData:data toFile:fileName compressing:NO encrypting:NO];
}

- (NSData*)loadDataFromFile:(NSString*)fileName {
  LC_CHECK_NOT_EMPTY(fileName);
  return [self loadDataFromFile:fileName compressing:NO encrypting:NO];
}

- (void)removeFile:(NSString*)fileName {
  LC_CHECK_NOT_EMPTY(fileName);
  NSString* filePath =
      [self.rootDirectory stringByAppendingPathComponent:fileName];
  if ([self.fileManager fileExistsAtPath:filePath]) {
    if (![self.fileManager removeItemAtPath:filePath error:nil]) {
      @throw [LcPersistenceException
          exceptionWithReason:[NSString stringWithFormat:@"persistence delete "
                                                         @"file failed, "
                                                         @"fileName=%@",
                                                         fileName]];
    }
  }
}

- (void)saveData:(NSData*)data
          toFile:(NSString*)fileName
     compressing:(BOOL)compressing
      encrypting:(BOOL)encrypting {
  LC_CHECK_NOT_EMPTY(data);
  LC_CHECK_NOT_EMPTY(fileName);
  if (compressing) {
    data = [self compress:data];
  }
  if (encrypting) {
    data = [self encrypt:data];
  }
  NSString* filePath =
      [self.rootDirectory stringByAppendingPathComponent:fileName];
  NSString* directory = [filePath stringByDeletingLastPathComponent];
  @synchronized(self) {
    if (![self.fileManager fileExistsAtPath:directory]) {
      LC_CHECK_TRUE([self.fileManager createDirectoryAtPath:directory
                                withIntermediateDirectories:YES
                                                 attributes:nil
                                                      error:nil]);
    }
    LC_CHECK_TRUE([data writeToFile:filePath atomically:YES]);
  }
}

- (NSData*)loadDataFromFile:(NSString*)fileName
                compressing:(BOOL)compressing
                 encrypting:(BOOL)encrypting {
  NSString* filePath =
      [self.rootDirectory stringByAppendingPathComponent:fileName];
  BOOL isDirectory = NO;
  if ([self.fileManager fileExistsAtPath:filePath isDirectory:&isDirectory]) {
    if (!isDirectory) {
      NSData* data = [NSData dataWithContentsOfFile:filePath];
      if (encrypting) {
        data = [self decrypt:data];
      }
      if (compressing) {
        data = [self uncompress:data];
      }
      return data;
    }
  }
  return nil;
}

- (NSData*)compress:(NSData*)data {
  NSData* deflated = [self deflateByZlib:data gzip:NO];
  if (deflated == nil) {
    @throw [LcPersistenceException exceptionWithReason:@"deflate error"];
  }
  return deflated;
}

- (NSData*)compressGzip:(NSData*)data {
  NSData* deflated = [self deflateByZlib:data gzip:YES];
  if (deflated == nil) {
    @throw [LcPersistenceException exceptionWithReason:@"deflate gzip error"];
  }
  return deflated;
}

- (NSData*)uncompress:(NSData*)data {
  NSData* inflated = [self inflateByZlib:data gzip:NO];
  if (inflated == nil) {
    @throw [LcPersistenceException exceptionWithReason:@"inflate error"];
  }
  return inflated;
}

- (NSData*)uncompressGzip:(NSData*)data {
  NSData* inflated = [self inflateByZlib:data gzip:YES];
  if (inflated == nil) {
    @throw [LcPersistenceException exceptionWithReason:@"inflate gzip error"];
  }
  return inflated;
}

- (NSData*)encrypt:(NSData*)data {
  return [self.securityCenter encrypt:data
                        withAes128Key:self.aesKey
                                   iv:self.aesIv];
}

- (NSData*)decrypt:(NSData*)data {
  return [self.securityCenter decrypt:data
                        withAes128Key:self.aesKey
                                   iv:self.aesIv];
}

- (NSData*)deflateByZlib:(NSData*)data gzip:(BOOL)gzip {
  size_t dataLength = data.length;

  z_stream stream;
  stream.next_in = (Bytef*)data.bytes;
  stream.avail_in = (uInt)data.length;
  stream.total_out = 0;
  stream.zalloc = Z_NULL;
  stream.zfree = Z_NULL;
  stream.opaque = Z_NULL;

  int windowBits = gzip ? 15 + 16 : -15;
  if (deflateInit2(&stream, Z_BEST_COMPRESSION, Z_DEFLATED, windowBits, 8,
                   Z_DEFAULT_STRATEGY) != Z_OK) {
    return nil;
  }

  size_t estimateCompressedLength = MAX(dataLength / 2, 16);
  NSMutableData* output =
      [NSMutableData dataWithLength:estimateCompressedLength];
  BOOL done = NO;
  while (!done) {
    int flush = stream.avail_in == 0 ? Z_FINISH : Z_NO_FLUSH;
    stream.next_out = output.mutableBytes + stream.total_out;
    stream.avail_out = (uInt)(output.length - stream.total_out);
    int status = deflate(&stream, flush);
    if (flush == Z_FINISH) {
      if (status == Z_STREAM_END) {
        done = YES;
      } else if (status == Z_OK) {
        if (stream.avail_out == 0) {
          [output increaseLengthBy:estimateCompressedLength];
        }
      } else {
        // error
        break;
      }
    } else {
      if (status == Z_OK) {
        if (stream.avail_out == 0) {
          [output increaseLengthBy:estimateCompressedLength];
        }
      } else {
        // error
        break;
      }
    }
  }
  if (deflateEnd(&stream) != Z_OK) {
    return nil;
  }
  if (!done) {
    return nil;
  }
  output.length = stream.total_out;
  return output;
}

- (NSData*)inflateByZlib:(NSData*)data gzip:(BOOL)gzip {
  size_t dataLength = data.length;

  z_stream stream;
  stream.next_in = (Bytef*)data.bytes;
  stream.avail_in = (uInt)data.length;
  stream.total_out = 0;
  stream.zalloc = Z_NULL;
  stream.zfree = Z_NULL;
  stream.opaque = Z_NULL;
  int windowBits = gzip ? 15 + 32 : -15;
  if (inflateInit2(&stream, windowBits) != Z_OK) {
    return nil;
  }

  // estimate twice as the compressed length
  NSMutableData* output = [NSMutableData dataWithLength:dataLength * 2];
  BOOL done = NO;
  while (!done) {
    stream.next_out = output.mutableBytes + stream.total_out;
    stream.avail_out = (uInt)(output.length - stream.total_out);
    int status = inflate(&stream, Z_NO_FLUSH);
    if (status == Z_STREAM_END) {
      done = YES;
    } else if (status == Z_OK) {
      if (stream.avail_out == 0) {
        [output increaseLengthBy:dataLength];
      }
    } else {
      // error
      break;
    }
  }
  if (inflateEnd(&stream) != Z_OK) {
    return nil;
  }
  if (!done) {
    return nil;
  }
  output.length = stream.total_out;
  return output;
}

- (NSArray*)fileNamesInFolder:(NSString*)folerName {
  LC_CHECK_NOT_EMPTY(folerName);
  NSString* filePath =
      [self.rootDirectory stringByAppendingPathComponent:folerName];
  BOOL isDirectory = NO;
  if (![self.fileManager fileExistsAtPath:filePath isDirectory:&isDirectory]) {
    return nil;
  }
  if (!isDirectory) {
    return nil;
  }
  return [self.fileManager contentsOfDirectoryAtPath:filePath error:nil];
}

- (BOOL)fileExists:(NSString*)fileName {
  LC_CHECK_NOT_EMPTY(fileName);
  NSString* filePath =
      [self.rootDirectory stringByAppendingPathComponent:fileName];
  return [self.fileManager fileExistsAtPath:filePath isDirectory:NULL];
}

- (void)updateFreeDiskSpace {
  self->_outOfFreeDiskSpace = self.freeDiskSpaceInBytes < 100 * 1024 * 1024;
}

- (long long)freeDiskSpaceInBytes {
  struct statfs buf;
  long long freespace = 0L;
  if (statfs(self.rootDirectory.UTF8String, &buf) >= 0) {
    freespace = (long long)(buf.f_bsize * buf.f_bfree);
  }
  LcLog(@"Free disk space is %lldM", freespace / 1024 / 1024);
  return freespace;
}

- (void)moveUrl:(NSURL*)url toFile:(NSString*)relativePath {
  LC_CHECK_NOT_NULL(url);
  LC_CHECK_NOT_EMPTY(relativePath);
  NSString* newFilePath =
      [self.rootDirectory stringByAppendingPathComponent:relativePath];
  BOOL result = [self.fileManager
      moveItemAtURL:url
              toURL:[NSURL
                        URLWithString:[NSString stringWithFormat:@"file://%@",
                                                                 newFilePath]]
              error:nil];
  (void)result;
#ifdef DEBUG
  LC_CHECK_TRUE(result);
#endif
}

- (void)moveFile:(NSString*)srcRelativePath toFile:(NSString*)destRelativePath {
  LC_CHECK_NOT_EMPTY(srcRelativePath);
  LC_CHECK_NOT_EMPTY(destRelativePath);
  NSString* srcFullPath = [self.rootDirectory stringByAppendingPathComponent:srcRelativePath];
  NSString* destFullPath = [self.rootDirectory stringByAppendingPathComponent:destRelativePath];
  if ([self.fileManager fileExistsAtPath:destFullPath]) {
    return;
  }
  @synchronized(self) {
    LC_CHECK_TRUE([self.fileManager fileExistsAtPath:srcFullPath]);
    BOOL result = [self.fileManager moveItemAtPath:srcFullPath toPath:destFullPath error:nil];
    (void)result;
#ifdef DEBUG
    LC_CHECK_TRUE(result);
#endif
  }
}

@end
